package com.googlecode.cswish.security;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.log4j.Logger;
import org.apache.struts2.StrutsConstants;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.security.type.Calculation;
import com.googlecode.cswish.security.type.RecordRuleSetOr;
import com.googlecode.cswish.security.type.RuleSetOr;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.Functions;
import com.opensymphony.xwork2.inject.Inject;


/**
 * @author Jerry.Feng Date: 2004-10-25 18:47:59
 * @version $id: $
 */
public class SafeManager {
    /**
     * Logger for this class
     */
    private static final Logger logger = Logger.getLogger(SafeManager.class);
    
    private boolean PERMISSION_CHECK_ENABLE;
    private String FREE_PATH;
    private String FORBIDDEN_PATH;
    public String warningPage;
    public String loginPage;
    
    @Resource
    private Config config;
    
    @Resource
    private Functions functions;
    
    @Resource
    private Routing routing;
    
    @Resource
    private BeanIntrospector beanIntrospector;
    
    @Inject(StrutsConstants.STRUTS_ACTION_EXTENSION)
	protected String actionExtension = "html";
    
    @PostConstruct
    public void init() {
    	PERMISSION_CHECK_ENABLE = config.isSecurityEnable();
    	FREE_PATH = config.getSecurityFreePath();
    	FORBIDDEN_PATH = config.getSecurityforbidPath();
    	
    	warningPage = config.getSecurityWarningPage() + '.' + actionExtension;
    	if (!warningPage.startsWith(FREE_PATH)) {
    		warningPage = FREE_PATH + warningPage;
    	}
    	loginPage = config.getSecurityLoginPage() + '.' + actionExtension;
    	if (!loginPage.startsWith(FREE_PATH)) {
    		loginPage = FREE_PATH + loginPage;
    	}
    }
    
    /**
     * <userId, Map<pathId, check condition>>
     * 
     * TODO: How to release user permission information? - clean it when logout? 
     */
    private Map<String, Map<String, Permission>> pathPermissions = new HashMap<String, Map<String, Permission>>();
   
    //private static final int FREE_PATH_LEN = FREE_PATH.length();
    //private static final int FORBIDDEN_PATH_LEN = FORBIDDEN_PATH.length();
    private static SafeManager manager = new SafeManager();
    public static SafeManager getInstance() {
        return manager;
    }
    private SafeManager() {        
    }

    public boolean isEnablePermissionCheck() {
    	return PERMISSION_CHECK_ENABLE;
    }
    
    public boolean check(HttpServletRequest request,HttpServletResponse response) {
    	if (!PERMISSION_CHECK_ENABLE) {
    		recordLastVisitInfo(request);
    		
    		if (getLoginUser(request) == null) {
    			// create login user for test
    			Invoker invoker = routing.path2Invoker(config.getSecurityLoginPage().substring(1));
    			String simpleName = beanIntrospector.getSimpleName(invoker.actionName);
    			functions.service(simpleName, invoker.getSubmitMethod(), null);
    		}
    		return true;
    	}
    	
        String path = request.getServletPath();
        
        boolean pass;
        String forwardPage = null;
        if (isFreeResource(path)) {
            pass = true;
        } else if (isForbiddenResource(path)) {
            pass = false;
            forwardPage = warningPage;
        } else {
        	UserInfo userInfo = getLoginUser(request);
            if (userInfo == null) {
            	recordLastVisitInfo(request);
                forwardPage = loginPage;
                pass = false;
            } else {
        		pass = checkPage(userInfo.getLoginId(), path, null);

	            if (!pass) {
	                forwardPage = warningPage;	            
            	}
            }
         }
        
        // subject is not permitted; redirect to error page
        if (!pass) {
            assert (forwardPage != null && forwardPage.startsWith(FREE_PATH));
            // TODO: if (forwardPage != loginPage) log it ?
            try {
            	//RequestDispatcher rd = request.getRequestDispatcher(forwardPage);
            	request.setAttribute("fr_action_path", path);
            	//rd.forward(request, response);
            	response.sendRedirect(request.getContextPath() + forwardPage);  
            	logger.info("forbid visiting page: " + path + ", forward page to:" + forwardPage);
            } catch (Exception ex) {
                logger.error(ex);
            }
        }
        
        return pass;
    }
    
    public void redirectWarningPage(HttpServletRequest request, HttpServletResponse response) {
    	try {
    		response.sendRedirect(request.getContextPath() + warningPage);
    	} catch (Exception ex) {
            logger.error(ex);
        }	
    }
    
    public void login(HttpServletRequest request, UserInfo userInfo) {
    	HttpSession session = request.getSession(true);
    	session.setAttribute(FrameConstant.SESSION_USER_INFO, userInfo);
    }
    
    /**
     * NOTICE:
     * this method should be called by "session filter" in order to avoid memory leak
     * 
     * @param request
     */
    public void logout(HttpServletRequest request) {
    	synchronized (request.getSession()) {
    		UserInfo userInfo = getLoginUser(request);
        	if (userInfo != null) {
        		logout(userInfo.getLoginId());
        	}
        	HttpSession session = request.getSession();
        	Enumeration enumeration = session.getAttributeNames();
        	while (enumeration.hasMoreElements()) {
        		session.removeAttribute((String) enumeration.nextElement());
        	}
		}
    }

    public UserInfo getLoginUser(HttpServletRequest request) {
    	if (request == null) {
    		return null;
    	}
    	
    	HttpSession session = request.getSession(true);
    	UserInfo user = (UserInfo) session.getAttribute(FrameConstant.SESSION_USER_INFO);
    	
    	if (user == null && config.getSecurityDomain() != null) {
    		// try to enable user login by domain token
    		
    		
    	}
        return user;
    }
    
    public String getLastVisitInfo(HttpServletRequest request) {
    	return (String)request.getSession().getAttribute(
    				FrameConstant.SESSION_LAST_VISIT_INFO);
    }
    
    /**
	 * @param request
	 */
	private void recordLastVisitInfo(HttpServletRequest request) {
		String skip = request.getParameter(FrameConstant.PARAM_SKIP_RECORD);
		if ("true".equals(skip)) {
			return;
		}
		
		// 1. path
		StringBuffer visitInfo = new StringBuffer(); 
		// visitInfo.append(request.getRequestURI());
		visitInfo.append(request.getServletPath());
		
		// 2. param
		StringBuffer paramInfo = new StringBuffer();
		Enumeration enu = request.getParameterNames();
		while(enu.hasMoreElements()) {
			String name = (String)enu.nextElement();
			String [] values = request.getParameterValues(name);
			if (values != null) {
				for (int i = 0; i < values.length; i++) {
					paramInfo.append('&').append(name).append('=').append(values[i]);
				}
			}
		}
		if (paramInfo.length() > 0) {
			visitInfo.append('?').append(paramInfo.substring(1));
		}
		
		// 3. cookie		
		request.getSession().setAttribute(
			FrameConstant.SESSION_LAST_VISIT_INFO,visitInfo.toString());
	}
    
    public boolean isFreeResource(String path) {
        return path.startsWith(FREE_PATH)? true:false;
    }
    
    public boolean isForbiddenResource(String path) {
        return path.startsWith(FORBIDDEN_PATH)? true:false;
    }

    /**
     * logout user information
     * 
     * @param userId
     */
    public void logout(String userId) {
    	pathPermissions.remove(userId);
    }
    
    private Permission getPagePermission(String userId, String path, boolean autoCreate) {
    	Map<String, Permission> pathPermission = pathPermissions.get(userId);
		
		if (pathPermission == null) {
			pathPermission = new HashMap<String, Permission>();
			pathPermissions.put(userId, pathPermission);
		}
		
		Permission permission = pathPermission.get(path);
		if (autoCreate && permission == null) {
			permission = new Permission();
			pathPermission.put(path, permission);
		}
		return permission;
    }
    
    /**
     * add page permission
     * 
     * @param userId
     * @param path
     */
	public void addPagePermission(String userId,
			String path) {
		addPagePermission(userId, path, Calculation.TRUE);
	}
	
	public Permission addPagePermission(String userId,
			String path, Calculation rule) {
		Permission permission = getPagePermission(userId, path, true);
		permission.addPathCondition(rule);
		
		// sub path extends all the permission comes from parent path
		Map<String, Permission> pathPermission = pathPermissions.get(userId);
		int index = -1;
		int lastIndex = path.length() - 1;
		// 1. get alias operations and add them to current path
		while ((index = path.indexOf('/', index + 1)) >= 0 && index < lastIndex) {
			String parentPath = path.substring(0, index + 1);
			Permission parentPermission = pathPermission.get(parentPath);
			if (parentPermission != null) {
				permission.addPathCondition(parentPermission.pathCondition);
			}
		}
		
		return permission;
	}
	
	/**
	 * Add the relevant page permission automatically when the field permission is authorized to user
	 * 
	 * @param userId
	 * @param requestId
	 * @param fieldName
	 * @param isRead
	 * @param rule
	 */
	public Permission addFieldLimit(String userId, String path,
			String fieldName, Calculation rule, boolean isRead) {
		Permission permission = getPagePermission(userId, path, false);
		if (permission == null) {
			permission = getPagePermission(userId, path, true);
			permission.setPagePermission(false);
		}
		
//		if (permission.pathCondition != null) {
//			// current path has the whole page permission
//			return;
//		}
		
		permission.addFieldCondition(fieldName, rule, isRead);
		return permission;
	}
	
	/**
	 * add record permission
	 * 
	 * @param userId
	 * @param requestId
	 * @param rule
	 */
	public Permission addRecordLimit(String userId, String path,
			Calculation rule) {
		Permission permission = getPagePermission(userId, path, false);
		if (permission == null) {
			permission = getPagePermission(userId, path, true);
			permission.setPagePermission(false);
		}
//		if (permission.pathCondition != null) {
//			// current path has the whole record permission
//			return;
//		}
		
		permission.addRecordCondition(rule);
		return permission;
	}
	
	/**
	 * verify page permission
	 * 
	 * @param userId
	 * @param requestId
	 * @param context
	 * @return
	 */
	public boolean checkPage(String userId, String path, Context context) {
		Map<String, Permission> pathPermission = pathPermissions.get(userId);
		if (pathPermission != null) {
			int index = 0;
			while(index >= 0) {
				Permission permission = pathPermission.get(path);
				if (permission != null && permission.isPagePermission() && (permission.pathCondition == null 
						|| permission.pathCondition.executeBoolean(context))) {
					return true;
				}
				
				// find the parent path
				boolean foundMethod = false;
				if (index == 0) { // skip the method if the method is found
					index = path.lastIndexOf('.');
					if (index > 0) {
						// method ends with '.'
						index = path.lastIndexOf('.', index - 1);
						foundMethod = index > 0;
					}
				}
				if (!foundMethod) {
					// skip the last path
					index = path.lastIndexOf('/', path.length() - 2);
				}
				
				if (index >= 0) {
					path = path.substring(0, index + 1);
				}
			}
		}
		return false;
	}
	
	/**
	 * list all the forbidden fields
	 * 
	 * @param userId
	 * @param requestId
	 * @param context
	 * @param fieldNames
	 * @return
	 */
	public List<String> listForbidWriteFields(String userId, String path,
			Context context, String ... fieldNames) {
		// TODO: environment information: context
		
		List<String> fields = new ArrayList<String>();
		Permission permission = getPagePermission(userId, path, false);
		if (permission != null) {
			Map<String, RuleSetOr> fieldConditions = permission.fieldWriteCondition;
			if (fieldConditions != null) {
				for (String fieldName : fieldNames) {
					RuleSetOr fieldPermission = fieldConditions.get(fieldName);
					if (fieldPermission != null) {
						boolean ret = fieldPermission.executeBoolean(context);
						if (!ret) {
							fields.add(fieldName);
						}
					}
				}
			}
		} else {
			if (logger.isDebugEnabled()) {
				logger.debug("User hasn't the page permission:" + path);
			}
			Collections.addAll(fields, fieldNames);
		}
		return fields;
	}
	
	/**
	 * verify field read permission
	 * 
	 * @param userId
	 * @param requestId
	 * @param context
	 * @param fieldNames
	 */
	public String checkReadField(String userId, String path,
			Context context, boolean executeCondition, String fieldPrefix, String ... fieldNames) {
		// TODO: environment information: context
		Permission permission = getPagePermission(userId, path, false);
		
		if (permission != null) {
			Map<String, RuleSetOr> fieldConditions = permission.fieldReadCondition;
			if (fieldConditions != null) {
				
				for (String fieldName : fieldNames) {
					RuleSetOr fieldPermission = fieldConditions.get(fieldName);
					if (fieldPermission != null) {
						if (executeCondition) {
							context.setFieldName(fieldPrefix == null ? fieldName : fieldPrefix + '.' + fieldName);
							return fieldPermission.execute(context);
						}
					}
				}
			}
		}
		return null;
	}

	/**
	 * verify field write permission
	 * 
	 * @param userId
	 * @param requestId
	 * @param context
	 * @param fieldNames
	 * @return
	 */
	public boolean checkWriteField(String userId, String path,
			Context context, boolean executeCondition, String fieldPrefix, String ... fieldNames) {
		// TODO: environment information: context
		Permission permission = getPagePermission(userId, path, false);
		
		boolean foundFieldLimitation = false;
		if (permission != null) {
			Map<String, RuleSetOr> fieldConditions = permission.fieldWriteCondition;
			if (fieldConditions != null) {
				for (String fieldName : fieldNames) {
					RuleSetOr fieldCondition = fieldConditions.get(fieldName);
					if (fieldCondition != null) {
						foundFieldLimitation = true;
						if (executeCondition) {
							context.setFieldName(fieldPrefix == null ? fieldName : fieldPrefix + '.' + fieldName);
							fieldCondition.executeBoolean(context);
						}
					}
				}
			}
		}
		return !foundFieldLimitation;
	}
	/**
	 * get the filter condition
	 * 
	 * @param userId
	 * @param requestId
	 * @param context
	 * @return
	 */
	public String checkResult(String userId, String path, Context context) {
		// TODO: environment information: context
		Map<String, Permission> pathPermission = pathPermissions.get(userId);
		if (pathPermission != null) {
			Permission permission = pathPermission.get(path);
			if (permission != null && permission.recordCondition != null) {
				return permission.recordCondition.execute(context);
			}
		}
		return null;
	}
	
	public class Permission {
		
		private boolean pagePermission = true;

		private Map<String, RuleSetOr> fieldReadCondition;
		
		private Map<String, RuleSetOr> fieldWriteCondition;
		
		private RuleSetOr pathCondition;
		
		private RecordRuleSetOr recordCondition;
		
		public Permission() {
		}
		
		private void addPathCondition(Calculation condition) {
			if (pathCondition == null) {
				if (condition instanceof RuleSetOr) {
					pathCondition = (RuleSetOr) condition;
				} else {
					pathCondition = new RuleSetOr();
					pathCondition.addRule(condition);
				}
			} else {
				pathCondition.addRule(condition);
			}
			this.fieldReadCondition = null;
			this.fieldReadCondition = null;
			this.recordCondition = null;
		}
		
		public boolean isPagePermission() {
			return pagePermission;
		}

		public void setPagePermission(boolean pagePermission) {
			this.pagePermission = pagePermission;
		}

		private void addRecordCondition(Calculation condition) {
			if (recordCondition == null) {
				recordCondition = new RecordRuleSetOr();
			}
			recordCondition.addRule(condition);
		}
		
		private void addFieldCondition(String fieldName, Calculation condition, boolean isRead) {
			Map<String, RuleSetOr> fieldCondition = isRead ? fieldReadCondition : fieldWriteCondition;
			if (isRead) {
				if (fieldReadCondition == null) {
					fieldReadCondition = new HashMap<String, RuleSetOr>();
				}
				fieldCondition = fieldReadCondition;
			} else {
				if (fieldWriteCondition == null) {
					fieldWriteCondition = new HashMap<String, RuleSetOr>();
				}
				fieldCondition = fieldWriteCondition;
			}
			

			RuleSetOr fieldPermission = fieldCondition.get(fieldName);
			if (fieldPermission == null) {
				fieldPermission = new RuleSetOr();
				fieldCondition.put(fieldName, fieldPermission);
			}
			fieldPermission.addRule(condition);
		}
	}
}